package cn.imatu.framework.log;

import cn.imatu.framework.core.http.HttpRequestServletManager;
import cn.imatu.framework.core.utils.IpUtils;
import cn.imatu.framework.core.utils.servlet.ServletUtils;
import cn.imatu.framework.core.utils.spring.SpringUtils;
import cn.imatu.framework.log.annotation.LogEvent;
import cn.imatu.framework.log.annotation.OperateLog;
import cn.imatu.framework.log.common.AccessLogConfig;
import cn.imatu.framework.log.common.BizStatus;
import cn.imatu.framework.log.event.WebLogEvent;
import cn.imatu.framework.log.utils.MDCTraceUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * 全局日志打印拦截器
 *
 * @author shenguangyang
 */
@Slf4j
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class LyAccessLogAspect {
    @Resource
    private LyLogProperties logProperties;
    @Resource
    private AccessLogConfig logConfig;

    // 表达式解析模板，在 {{  }} 中的内容，会被当作 SpEL 表达式进行解析
    static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext("{{", "}}");

    static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
    @Pointcut("execution(public * *..*.*Controller.*(..))")
    public void webLog() {
    }

    @Around("webLog()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        if (!logProperties.getEnableWebLog()) {
            return point.proceed();
        }
        // 设置方法名称
        String className = point.getTarget().getClass().getName();
        String methodName = point.getSignature().getName();

        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Object[] args = point.getArgs();

        Object result = null;
        LogEvent logEventAnnot = point.getTarget().getClass().getAnnotation(LogEvent.class);
        OperateLog operateLogAnnot = getMethodAnnot(point);

        WebLogInfo.WebLogInfoBuilder builder = WebLogInfo.builder().status(BizStatus.OK);
        StopWatch watch = StopWatch.createStarted();

        String content = Optional.ofNullable(operateLogAnnot).map(OperateLog::content).orElse("");

        // 初始日志收集参数
        HttpServletRequest request = ServletUtils.getRequest().orElse(null);
        if (request == null) {
            return point.proceed();
        }
        try {
            // 请求的地址
            String ip = IpUtils.getRequestIp(new HttpRequestServletManager(request));

            String eventType = Optional.ofNullable(operateLogAnnot).map(OperateLog::eventType).filter(StringUtils::isNotEmpty).orElse(logConfig.getDefaultEventType());
            String eventTypeName = Optional.ofNullable(operateLogAnnot).map(OperateLog::name).filter(StringUtils::isNotEmpty).orElse(null);
            String oprType = Optional.ofNullable(operateLogAnnot).map(OperateLog::oprType).filter(StringUtils::isNotEmpty).orElse(logConfig.getDefaultOprType());
            String eventObject = Optional.ofNullable(logEventAnnot).map(LogEvent::eventObject).filter(StringUtils::isNotEmpty).orElse(null);
            String eventObjectName = Optional.ofNullable(logEventAnnot).map(LogEvent::name).filter(StringUtils::isNotEmpty).orElse(null);
            String url = request.getRequestURI();
            builder.ip(ip).url(url).respData(result)
                .operateLogAnnot(operateLogAnnot).logEventAnnot(logEventAnnot).traceId(MDCTraceUtils.getTraceId())
                .eventType(eventType).eventTypeName(eventTypeName)
                .eventObject(eventObject).eventObjectName(eventObjectName).oprType(oprType).content(content)
                .httpMethod(request.getMethod()).classMethod(className + "." + methodName + "()")
                .proceedingJoinPoint(point).contentType(request.getContentType());

            // 参数值
            Object[] argsValue = point.getArgs();
            builder.reqData(argsArrayToList(argsValue));
        } catch (Exception e) {
            log.error("error", e);
        }

        WebLogInfo logInfo = builder.build();
        try {
            if (Objects.nonNull(operateLogAnnot)) {
                LogContext.initParams();
            }
            result = point.proceed();
            logInfo.setRespData(result);
            return result;
        } catch (Throwable e) {
            logInfo.setStatus(BizStatus.FAIL);
            logInfo.setErrorMsg(e.getMessage());
            logInfo.setThrowable(e);
            throw e;
        } finally {
            try {
                // step 使用spel解析参数, 使用变量方式传入业务动态数据
                if (operateLogAnnot != null && content.contains("#")) {
                    // 获取被拦截方法参数名列表(使用Spring支持类库)
                    LocalVariableTableParameterNameDiscoverer localVariableTable = new LocalVariableTableParameterNameDiscoverer();
                    String[] paraNameArr = localVariableTable.getParameterNames(method);
                    StandardEvaluationContext context = new StandardEvaluationContext();
                    for (int i = 0; paraNameArr != null && i < paraNameArr.length; i++) {
                        context.setVariable(paraNameArr[i], args[i]);
                    }
                    LogContext.params().forEach((k, v) -> context.setVariable("ctx_" + k, v));
                    content = EXPRESSION_PARSER.parseExpression(content, TEMPLATE_PARSER_CONTEXT).getValue(context, String.class);
                }
            } catch (Exception e) {
                log.error("url: {}, error: ", logInfo.getUrl(), e);
            } finally {
                LogContext.clean();
            }

            try {
                logInfo.setCostTime(watch.getTime(TimeUnit.MILLISECONDS));
                logInfo.setContent(content);
                Consumer<WebLogInfo> reqLogHandle = logConfig.getReqLogHandle();
                if (Objects.nonNull(reqLogHandle)) {
                    reqLogHandle.accept(logInfo);
                }

                Consumer<WebLogInfo> respLogHandle = logConfig.getRespLogHandle();
                if (Objects.nonNull(respLogHandle)) {
                    respLogHandle.accept(logInfo);
                }
                if (operateLogAnnot != null && logEventAnnot != null) {
                    SpringUtils.getContext().publishEvent(new WebLogEvent(logInfo));
                }

            } catch (Exception e) {
                log.error("error: ", e);
                logInfo.setStatus(BizStatus.FAIL);
                logInfo.setErrorMsg(e.getMessage());
                logInfo.setThrowable(e);
                Consumer<WebLogInfo> respLogHandle = logConfig.getRespLogHandle();
                if (Objects.nonNull(respLogHandle)) {
                    respLogHandle.accept(logInfo);
                }
                SpringUtils.getContext().publishEvent(new WebLogEvent(logInfo));
            } finally {
                watch.stop();
            }
        }
    }

    public OperateLog getMethodAnnot(ProceedingJoinPoint point) {
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(OperateLog.class);
        }
        return null;
    }

    /**
     * 参数拼装
     */
    private List<Object> argsArrayToList(Object[] paramsArray) {
        if (paramsArray != null && paramsArray.length > 0) {
            return Arrays.stream(paramsArray).filter(Objects::nonNull).filter(e -> !isFilterObject(e)).collect(Collectors.toList());
        }
        return Collections.emptyList();
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象，则返回true；否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
            || o instanceof BindingResult;
    }
}
